Profiler.componentWillUnmount   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 24
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 24
c 0
b 0
f 0
rs 9.45
cc 3
1
import { startInactiveSpan } from '@sentry/browser';
2
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, timestampInSeconds, withActiveSpan, spanToJSON } from '@sentry/core';
3
import * as React from 'react';
4
import { REACT_MOUNT_OP, REACT_UPDATE_OP, REACT_RENDER_OP } from './constants.js';
5
import { hoistNonReactStatics } from './hoist-non-react-statics.js';
6
7
const UNKNOWN_COMPONENT = 'unknown';
8
9
/**
10
 * The Profiler component leverages Sentry's Tracing integration to generate
11
 * spans based on component lifecycles.
12
 */
13
class Profiler extends React.Component {
14
  /**
15
   * The span of the mount activity
16
   * Made protected for the React Native SDK to access
17
   */
18
19
  /**
20
   * The span that represents the duration of time between shouldComponentUpdate and componentDidUpdate
21
   */
22
23
   constructor(props) {
24
    super(props);
25
    const { name, disabled = false } = this.props;
26
27
    if (disabled) {
28
      return;
29
    }
30
31
    this._mountSpan = startInactiveSpan({
32
      name: `<${name}>`,
33
      onlyIfParent: true,
34
      op: REACT_MOUNT_OP,
35
      attributes: {
36
        [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler',
37
        'ui.component_name': name,
38
      },
39
    });
40
  }
41
42
  // If a component mounted, we can finish the mount activity.
43
   componentDidMount() {
44
    if (this._mountSpan) {
45
      this._mountSpan.end();
46
    }
47
  }
48
49
   shouldComponentUpdate({ updateProps, includeUpdates = true }) {
50
    // Only generate an update span if includeUpdates is true, if there is a valid mountSpan,
51
    // and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive.
52
    // We are just trying to give baseline clues for further investigation.
53
    if (includeUpdates && this._mountSpan && updateProps !== this.props.updateProps) {
54
      // See what props have changed between the previous props, and the current props. This is
55
      // set as data on the span. We just store the prop keys as the values could be potentially very large.
56
      const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]);
57
      if (changedProps.length > 0) {
58
        const now = timestampInSeconds();
59
        this._updateSpan = withActiveSpan(this._mountSpan, () => {
60
          return startInactiveSpan({
61
            name: `<${this.props.name}>`,
62
            onlyIfParent: true,
63
            op: REACT_UPDATE_OP,
64
            startTime: now,
65
            attributes: {
66
              [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler',
67
              'ui.component_name': this.props.name,
68
              'ui.react.changed_props': changedProps,
69
            },
70
          });
71
        });
72
      }
73
    }
74
75
    return true;
76
  }
77
78
   componentDidUpdate() {
79
    if (this._updateSpan) {
80
      this._updateSpan.end();
81
      this._updateSpan = undefined;
82
    }
83
  }
84
85
  // If a component is unmounted, we can say it is no longer on the screen.
86
  // This means we can finish the span representing the component render.
87
   componentWillUnmount() {
88
    const endTimestamp = timestampInSeconds();
89
    const { name, includeRender = true } = this.props;
90
91
    if (this._mountSpan && includeRender) {
92
      const startTime = spanToJSON(this._mountSpan).timestamp;
93
      withActiveSpan(this._mountSpan, () => {
94
        const renderSpan = startInactiveSpan({
95
          onlyIfParent: true,
96
          name: `<${name}>`,
97
          op: REACT_RENDER_OP,
98
          startTime,
99
          attributes: {
100
            [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler',
101
            'ui.component_name': name,
102
          },
103
        });
104
        if (renderSpan) {
105
          // Have to cast to Span because the type of _mountSpan is Span | undefined
106
          // and not getting narrowed properly
107
          renderSpan.end(endTimestamp);
108
        }
109
      });
110
    }
111
  }
112
113
   render() {
114
    return this.props.children;
115
  }
116
}
117
118
// React.Component default props are defined as static property on the class
119
Object.assign(Profiler, {
120
  defaultProps: {
121
    disabled: false,
122
    includeRender: true,
123
    includeUpdates: true,
124
  },
125
});
126
127
/**
128
 * withProfiler is a higher order component that wraps a
129
 * component in a {@link Profiler} component. It is recommended that
130
 * the higher order component be used over the regular {@link Profiler} component.
131
 *
132
 * @param WrappedComponent component that is wrapped by Profiler
133
 * @param options the {@link ProfilerProps} you can pass into the Profiler
134
 */
135
// eslint-disable-next-line @typescript-eslint/no-explicit-any
136
function withProfiler(
137
  WrappedComponent,
138
  // We do not want to have `updateProps` given in options, it is instead filled through the HOC.
139
  options,
140
) {
141
  const componentDisplayName =
142
    options?.name || WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT;
143
144
  const Wrapped = (props) => (
145
    React.createElement(Profiler, { ...options, name: componentDisplayName, updateProps: props,}
146
      , React.createElement(WrappedComponent, { ...props,} )
147
    )
148
  );
149
150
  Wrapped.displayName = `profiler(${componentDisplayName})`;
151
152
  // Copy over static methods from Wrapped component to Profiler HOC
153
  // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
154
  hoistNonReactStatics(Wrapped, WrappedComponent);
155
  return Wrapped;
156
}
157
158
/**
159
 *
160
 * `useProfiler` is a React hook that profiles a React component.
161
 *
162
 * Requires React 16.8 or above.
163
 * @param name displayName of component being profiled
164
 */
165
function useProfiler(
166
  name,
167
  options = {
168
    disabled: false,
169
    hasRenderSpan: true,
170
  },
171
) {
172
  const [mountSpan] = React.useState(() => {
173
    if (options?.disabled) {
174
      return undefined;
175
    }
176
177
    return startInactiveSpan({
178
      name: `<${name}>`,
179
      onlyIfParent: true,
180
      op: REACT_MOUNT_OP,
181
      attributes: {
182
        [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler',
183
        'ui.component_name': name,
184
      },
185
    });
186
  });
187
188
  React.useEffect(() => {
189
    if (mountSpan) {
190
      mountSpan.end();
191
    }
192
193
    return () => {
194
      if (mountSpan && options.hasRenderSpan) {
195
        const startTime = spanToJSON(mountSpan).timestamp;
196
        const endTimestamp = timestampInSeconds();
197
198
        const renderSpan = startInactiveSpan({
199
          name: `<${name}>`,
200
          onlyIfParent: true,
201
          op: REACT_RENDER_OP,
202
          startTime,
203
          attributes: {
204
            [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler',
205
            'ui.component_name': name,
206
          },
207
        });
208
        if (renderSpan) {
209
          // Have to cast to Span because the type of _mountSpan is Span | undefined
210
          // and not getting narrowed properly
211
          renderSpan.end(endTimestamp);
212
        }
213
      }
214
    };
215
    // We only want this to run once.
216
    // eslint-disable-next-line react-hooks/exhaustive-deps
217
  }, []);
218
}
219
220
export { Profiler, UNKNOWN_COMPONENT, useProfiler, withProfiler };
221
//# sourceMappingURL=profiler.js.map
222